﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Perforce.P4;
using Red.Controls.Depot.FileViewItemsProviders;
using Red.Controls.Depot.ViewModels;
using Red.Controls.Extensions;
using Red.Controls.Shell;
using Red.Controls.Utils;
using Red.Controls.ViewModels;
using Red.Core;
using P4 = Red.Core.Perforce;

namespace Red.Controls.Depot
{
    public static class DepotFileViewCommands
    {
        public static RedCommand DoubleClick = new RedCommand();
        public static RedCommand CheckOut = new RedCommand( "Check Out", null );
        public static RedCommand GetLatestRevision = new RedCommand( "Get Latest Revision", null );
        public static RedCommand MarkForAdd = new RedCommand( "Mark for Add", null );
        public static RedCommand Delete = new RedCommand( "Delete", null );
        public static RedCommand Revert = new RedCommand( "Revert", null );
    }


    /// <summary>
    /// Interaction logic for DepotFileView.xaml
    /// </summary>
    public partial class DepotFileViewPage : Shell.Page
    {
        #region Data

        // Backing store for Files property.
        readonly ObservableCollection<FileViewItem> _files = new ObservableCollection<FileViewItem>();

        readonly ObservableCollection<CommandWrapper> _commands = new ObservableCollection<CommandWrapper>();

        // Utility dictionary to access FileViewItems by their paths faster.
        private Dictionary<string, FileViewItem> _filesDict = new Dictionary<string, FileViewItem>();

        // Temporarily keeps selected items' paths to be restored after refresh.
        private List<FileViewItem> _selectedFilesLatch = new List<FileViewItem>();

        private readonly BackgroundWorker _rebuildWorker = new BackgroundWorker();
        private const int _updatePackageSize = 100;
        private bool _pendingRebuildRequest;
        private FileViewItemsProvider _itemsProvider;
        private DepotInfo _depot;
        private string _scheduledSelectionPath;
        private ListBox _listBox;
        private bool _isRestoringSelection = false;
        private bool _rebuildRecursionGuard = false;
        private bool _selectionHandlerRecursionGuard = false;
        private bool _callSelectionHandlerScheduled = false;

        /// <summary>
        /// List of file paths that are currently updating. This items have IsUpdating property set to true.
        /// </summary>
        private readonly List<string> _updatingPaths = new List<string>();

        #endregion Data

        #region Properties

        /// <summary>
        /// List of all visible items in DepotFileView.
        /// </summary>
        public ObservableCollection<FileViewItem> Files
        {
            get { return _files; }
        }

        public IEnumerable<CommandWrapper> Commands
        {
            get { return _commands; }
        }

        /// <summary>
        /// When active page changes this property is used to refresh breadcrumb bar.
        /// </summary>
        public string BreadcrumbPath { get; set; }

        /// <summary>
        /// When active page changes this property is used to select corresponding node in depot tree.
        /// </summary>
        public string Path { get; set; }

        /// <summary>
        /// Current selection
        /// </summary>
        public IEnumerable<string> SelectedPaths
        {
            get
            {
                var selectedFiles = _listBox.SelectedItems.Cast<FileViewItem>();

                // Append "\..." to directories. This is the format that perforce requires when denoting directories so that it knows to recurse the directory structure
                var selectedPaths = from file in selectedFiles select ( file is FileViewItemDirectory )? file.Path + @"\..." : file.Path;
                return selectedPaths;
            }
        }

        public DepotInfo Depot
        {
            get { return _depot; }
            set
            {
                if ( _depot != value )
                {
                    _depot = value;
                    Monitor.SourceControl = value.SourceControl;

                    // Depot change probably _should_ call Rebuild, but for now it's done only once and I want to avoid calling
                    // Rebuild twice on initialization. For now just ensure ItemsProvider is set next.
                }
            }
        }

        public Core.SourceControl.Monitor Monitor { get; private set; } = new Core.SourceControl.Monitor();

        public FileViewItemsProvider ItemsProvider
        {
            get { return _itemsProvider; }
            set 
            {
                Trace.Assert( value != null, "Can't use null for ItemsProvider. Use EmptyItemsProvider instead!" );
                if ( _itemsProvider != value )
                {
                    _itemsProvider = value;
                    ResetView();
                    Rebuild( false );
                }
            }
        }

        #endregion Properties

        #region C-tor

        public DepotFileViewPage()
        {
            _rebuildWorker.WorkerReportsProgress = true;
            _rebuildWorker.WorkerSupportsCancellation = true;
            _rebuildWorker.DoWork += RebuildWorker_DoWork;
            _rebuildWorker.ProgressChanged += RebuildWorker_ProgressChanged;
            _rebuildWorker.RunWorkerCompleted += RebuildWorker_RunWorkerCompleted;

            InitializeComponent();

            Monitor.OperationStarted += Monitor_OperationStarted;
            Monitor.OperationFinished += Monitor_OperationFinished;
            Files.CollectionChanged += Files_CollectionChanged;
            Loaded += DepotFileViewPage_Loaded;
        }

        #endregion C-tor

        #region Public methods

        public void RefreshView()
        {
            Rebuild( true );
        }

        /// <summary>
        ///     Selects an item by the given path. Note that selection may be scheduled for later, if the background rebuild is in progress.
        /// </summary>
        public void SelectByPath( string path )
        {
            _scheduledSelectionPath = path;
            if ( ItemsProvider != null && !_rebuildWorker.IsBusy )
            {
                DoScheduledSelection();
            }
        }

        #endregion Public methods

        #region Events

        public delegate void ItemMouseEventHandler( object sender, FileViewItem item );

        public delegate void SelectionEventHandler( object sender, IList<FileViewItem> items );

        public event ItemMouseEventHandler ItemDoubleClick;

        public event SelectionEventHandler SelectedItemChanged;

        #endregion Events

        #region Private methods

        private void ResetView()
        {
            CollectionView collectionView = (CollectionView)CollectionViewSource.GetDefaultView( Files );
            collectionView.GroupDescriptions.Clear();

            if ( _listBox != null )
            {
                _listBox.SelectionChanged -= ListBox_SelectionChanged;
            }

            if ( ItemsProvider.ShowGroups )
            {
                collectionView.GroupDescriptions.Add( new PropertyGroupDescription( "Group" ) );
                _groupingFileListView.Visibility = Visibility.Visible;
                _groupingFileListView.SetBinding( ListBox.ItemsSourceProperty, new Binding( "Files" ) { Source = this } );
                _virtualizingfileListView.Visibility = Visibility.Collapsed;
                _virtualizingfileListView.ItemsSource = null;
                _listBox = _groupingFileListView;
            }
            else
            {
                _groupingFileListView.Visibility = Visibility.Collapsed;
                _groupingFileListView.ItemsSource = null;
                _virtualizingfileListView.Visibility = Visibility.Visible;
                _virtualizingfileListView.SetBinding( ListBox.ItemsSourceProperty, new Binding( "Files" ) { Source = this } ); ;
                _listBox = _virtualizingfileListView;
            }

            _listBox.SelectionChanged += ListBox_SelectionChanged;
        }

        private void DoScheduledSelection()
        {
            if ( _scheduledSelectionPath != null )
            {
                FileViewItem file = _files.FirstOrDefault( f => f.Path == _scheduledSelectionPath );
                if ( file != null )
                {
                    _listBox.ScrollIntoView( file );
                    _listBox.SelectedItems.Clear();
                    file.IsSelected = true;
                }
                _scheduledSelectionPath = null;
            }
        }

        private void Rebuild( bool preserveSelection )
        {
            if ( _rebuildRecursionGuard )
                return;

            _rebuildRecursionGuard = true;

            Debug.Print( "DepotFileViewPage.Rebuild" );

            if ( preserveSelection )
            {
                _selectedFilesLatch = _listBox.SelectedItems.Cast<FileViewItem>().ToList();
            }
            else
            {
                _selectedFilesLatch.Clear();
            }

            _files.Clear();
            _filesDict.Clear();
            RunRebuildWorker();

            _commands.Clear();
            foreach ( var cmd in ItemsProvider.Commands )
                _commands.Add( cmd );

            _rebuildRecursionGuard = false;
        }

        private void RunRebuildWorker()
        {
            if ( _rebuildWorker.IsBusy )
            {
                _rebuildWorker.CancelAsync();
                _pendingRebuildRequest = true;
            }
            else
                _rebuildWorker.RunWorkerAsync();
        }

        private void SetIsUpdatingState( FileViewItem fileViewItem )
        {
            if ( fileViewItem.IsOutOfDate )
                fileViewItem.IsUpdating = IsPathUpdating( fileViewItem.Path );
            else
                _updatingPaths.Remove( fileViewItem.Path );
        }

        private bool IsPathUpdating( string path )
        {
            return _updatingPaths.Contains( path );
        }
        
        private FileViewItem GetItemByPath( string path )
        {
            FileViewItem foundItem;
            _filesDict.TryGetValue( path, out foundItem );
            return foundItem;
        }

        private void SetItemIsUpdating( string path, bool value )
        {
            var foundItem = GetItemByPath( path );
            if ( foundItem == null )
                return;

            foundItem.IsUpdating = value;

            if ( value )
                _updatingPaths.Add( path );
            else
                _updatingPaths.Remove( path );        
        }

        private void SetItemP4FileState( string path, P4FileState p4FileState )
        {
            var foundItem = GetItemByPath( path );
            if ( foundItem == null )
                return;

            foundItem.P4FileState = p4FileState;
        }

        private void SetItemP4MetaData( string path, FileMetaData fileMetaData )
        {
            var foundItem = GetItemByPath( path );
            if ( foundItem == null )
                return;

            foundItem.P4MetaData = fileMetaData;
        }

        private void RemoveItem( string path )
        {
            var foundItem = GetItemByPath( path );
            if ( foundItem == null )
                return;

            Dispatcher.BeginInvoke( ( Action )delegate
            {
                _files.Remove( foundItem );
                _filesDict.Remove( path );
            } );
        }
        
        private async Task GetLatestSelectedItems()
        {
            await SourceControl.UIHelper.GetLatest( SelectedPaths, _depot.SourceControl );
        }

        private async Task CheckoutSelectedItems()
        {
            await SourceControl.UIHelper.Checkout( SelectedPaths, Window.GetWindow( this ), _depot.SourceControl );
        }

        private async Task RevertSelectedItems()
        {
            var selectedFiles = _listBox.SelectedItems.Cast<FileViewItem>();
            var selectedPaths = from file 
                                in selectedFiles
                                where file.P4FileState == P4FileState.P4FS_CheckedOut ||
                                      file.P4FileState == P4FileState.P4FS_MarkedForAdd ||
                                      file.P4FileState == P4FileState.P4FS_Deleted
                                select file.Path;

            if ( selectedPaths.Any() )
            {
                await SourceControl.UIHelper.Revert( selectedPaths, Window.GetWindow( this ), _depot.SourceControl );
                RefreshView();
            }
        }

        private async Task MarkForAddSelectedItems()
        {
            await SourceControl.UIHelper.MarkForAdd( SelectedPaths, Window.GetWindow( this ), _depot.SourceControl );
        }

        private async Task DeleteSelectedItems()
        {
            var selectedFiles = _listBox.SelectedItems.Cast<FileViewItem>();

            var inP4Files  = from file in selectedFiles
                             where file.P4FileState != P4FileState.P4FS_NotInDepot
                             select file.Path;

            var notP4Files = from file in selectedFiles
                             where file.P4FileState == P4FileState.P4FS_NotInDepot && !( file is FileViewItemDirectory )
                             select file.Path;

            var notP4Dirs  = from file in selectedFiles
                             where file.P4FileState == P4FileState.P4FS_NotInDepot && file is FileViewItemDirectory
                             select file.Path;

            if ( inP4Files.Any() )
                await SourceControl.UIHelper.Delete( SelectedPaths, Window.GetWindow( this ), _depot.SourceControl );

            if ( notP4Files.Any() || notP4Dirs.Any() )
            {
                if ( RedMessageBox.Show( this, RedDialogType.YesNo, "Are you sure to delete local file(s)?", "Delete local files" ) )
                {
                    foreach ( string file in notP4Files )
                        DeleteLocalFile( file );

                    foreach ( string dir in notP4Dirs )
                        DeleteDirectory( dir );

                    RefreshView();
                }
            }
        }

        private void DeleteLocalFile( string path )
        {
            try
            {
                System.IO.File.Delete( path );
            }
            catch ( SystemException ex )
            {
                RedMessageBox.Show( this, RedDialogType.Ok, ex.Message, "Cannot delete file" );
            }
        }

        private void DeleteDirectory( string path )
        {
            try
            {
                // NOTE: For now, only empty directories can be deleted. It's both safer and simpler :P
                if ( System.IO.Directory.GetFiles( path ).Length == 0 && System.IO.Directory.GetDirectories( path ).Length == 0 )
                    System.IO.Directory.Delete( path );
                else
                    RedMessageBox.Show( this, RedDialogType.Ok, "Cannot delete non-empty directory", "Cannot delete directory" );
            }
            catch ( SystemException ex )
            {
                RedMessageBox.Show( this, RedDialogType.Ok, ex.Message, "Cannot delete directory" );
            }
        }

        #endregion Private methods

        #region Event handlers

        private void DepotFileViewPage_Loaded( object s1, RoutedEventArgs e1 )
        {
            Window.GetWindow( this ).Closed += ( object sender, EventArgs e ) => Monitor.UnregisterListeners();
        }

        private void RebuildWorker_DoWork( object sender, DoWorkEventArgs doWorkEventArgs )
        {
            var files = new List<FileInfo>();
            var folders = new List<FolderInfo>();

            var selectedFiles = new List<FileInfo>();
            foreach ( FileViewItem item in _selectedFilesLatch )
            {
                if ( !(item is FileViewItemDirectory ) )
                    selectedFiles.Add( new FileInfo { FullPath = item.Path, Group = item.Group } );
            }

            ItemsProvider.EnumerateItems( folders, files, selectedFiles );

            int index = 0;

            // process all folders at once

            if ( folders.Any() )
            {
                var folderItems = ItemsProvider.ProcessFoldersBatch( folders, ref index );
                _rebuildWorker.ReportProgress( 0, folderItems );
            }

            // process files in batches

            while ( files.Any() )
            {
                if ( _rebuildWorker.CancellationPending )
                {
                    doWorkEventArgs.Cancel = true;
                    break;
                }

                var batchToProcess = files.Take( _updatePackageSize ).ToList();
                files = files.Skip( _updatePackageSize ).ToList();
                var fileItems = ItemsProvider.ProcessFilesBatch( batchToProcess, ref index );
                _rebuildWorker.ReportProgress( 0, fileItems );
            }
        }

        private void RebuildWorker_ProgressChanged( object sender, ProgressChangedEventArgs progressChangedEventArgs )
        {
            var updateItems = progressChangedEventArgs.UserState as IEnumerable<UpdateInfo>;
            foreach ( var updateItem in updateItems )
            {
                if ( _rebuildWorker.CancellationPending )
                    break;

                FileViewItem item = updateItem.CreateViewItem( _depot );
                SetIsUpdatingState( item );
                _files.Insert( updateItem.Index, item );

                Thread.Sleep( 0 );
            }
        }

        private void RebuildWorker_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs runWorkerCompletedEventArgs )
        {
            _filesDict = _files.ToDictionary( item => item.Path );
            if ( _pendingRebuildRequest )
            {
                _pendingRebuildRequest = false;
                Rebuild( true );
            }
            else if ( _scheduledSelectionPath == null )
            {   // restore selection from before refresh (if possible), but only if there is no pending user's selection
                _isRestoringSelection = true;
                _listBox.SelectedItems.Clear();
                foreach ( FileViewItem item in _selectedFilesLatch )
                {
                    FileViewItem itemToSelect = GetItemByPath( item.Path );
                    if ( itemToSelect != null )
                    {
                        _listBox.SelectedItems.Add( itemToSelect );
                    }
                }
                _selectedFilesLatch.Clear();
                _isRestoringSelection = false;
            }
            else
            {   // only if no more pending requests
                DoScheduledSelection();
            }
        }

        private void Files_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            // Let the source control monitor know which files we're interested in
            if ( e.Action == NotifyCollectionChangedAction.Add )
            {
                foreach ( FileViewItem file in e.NewItems )
                {
                    Monitor.Files.Add( file.Path );
                }
            }
            else if ( e.Action == NotifyCollectionChangedAction.Remove )
            {
                foreach ( FileViewItem file in e.OldItems )
                {
                    Monitor.Files.Remove( file.Path );
                }
            }
        }

        private void Monitor_OperationStarted( string file )
        {
            SetItemIsUpdating( file, true );
        }

        private void Monitor_OperationFinished( string file, FileMetaData metadata )
        {
            SetItemIsUpdating( file, false );
            if ( metadata == null )
            {
                SetItemP4FileState( file, P4FileState.P4FS_NotInDepot );
            }
            else if ( metadata.HeadAction == FileAction.Delete )
            {
                RemoveItem( file );
            }
            else
            {
                SetItemP4MetaData( file, metadata );
            }
        }


        private async void CheckOut_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            await CheckoutSelectedItems();
        }

        private void CheckOut_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            var selectedItems = _listBox.SelectedItems.Cast<FileViewItem>();
            e.CanExecute = selectedItems.All( item => !item.IsUpdating && item.P4FileState == P4FileState.P4FS_InDepot );
        }

        private async void GetLatestRevision_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            await GetLatestSelectedItems();
        }

        private void GetLatestRevision_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            var selectedItems = _listBox.SelectedItems.Cast<FileViewItem>();
            /*
             * File items have P4FileState. Directories does not exist for Perforce and they have state P4FileState.P4FS_NotInDepot.
             * That's why we need to add check "|| item is FileViewItemDirectory."
             */
            e.CanExecute = selectedItems.All( item => !item.IsUpdating && item.P4FileState == P4FileState.P4FS_InDepot || item is FileViewItemDirectory );
        }

        private async void MarkForAdd_Executed( object sender, ExecutedRoutedEventArgs e )        
        {
            await MarkForAddSelectedItems();
        }

        private void MarkForAdd_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            var selectedItems = _listBox.SelectedItems.Cast<FileViewItem>();
            e.CanExecute = selectedItems.All( item => !item.IsUpdating && item.P4FileState == P4FileState.P4FS_NotInDepot && item.IsVersionControlItem() );
        }

        private async void Revert_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            await RevertSelectedItems();
        }

        private void Revert_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            var selectedItems = _listBox.SelectedItems.Cast<FileViewItem>();
            e.CanExecute = selectedItems.All( item => !item.IsUpdating &&
                ( item.P4FileState == P4FileState.P4FS_CheckedOut || 
                  item.P4FileState == P4FileState.P4FS_MarkedForAdd ||
                  item.P4FileState == P4FileState.P4FS_Deleted ) );
        }

        private async void Delete_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            await DeleteSelectedItems();
        }

        private void Delete_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            var selectedItems = _listBox.SelectedItems.Cast<FileViewItem>();
            e.CanExecute = selectedItems.All( item => !item.IsUpdating && item.P4FileState != P4FileState.P4FS_Deleted );
        }

        private void DepotFileView_DragStart( object sender, DragStartEventArgs e )
        {
            ListBoxItem container = Helpers.FindVisualAncestorOfType<ListBoxItem>( e.OriginalSource );
            if ( container != null )
            {
                var item = container.DataContext as FileViewItem;
                e.DraggedData = Task.FromResult( item?.GetDraggedData() );
            }
            else
            {
                e.DraggedData = Task.FromResult<IDataObject>( null );
            }
        }

        private void Grid_MouseLeftButtonDown( object sender, MouseButtonEventArgs e )
        {
            if ( e.ClickCount == 2 )
            {
                var item = ( (FrameworkElement)e.OriginalSource ).DataContext as FileViewItem;
                if ( !item.IsUpdating )
                {
                    ItemDoubleClick?.Invoke( sender, item );
                }
            }
        }

        private void ListBox_SelectionChanged( object sender, SelectionChangedEventArgs e )
        {
            if ( _selectionHandlerRecursionGuard )
                return;

            _selectionHandlerRecursionGuard = true;

            if ( !_isRestoringSelection && ItemsProvider.ReEnumerateOnSelection )
            {
                Rebuild( true );
            }

            if ( !_isRestoringSelection || ( _isRestoringSelection && ItemsProvider.ReEnumerateOnSelection ) )
            {
                var selectionAsItems = _listBox.SelectedItems.Cast<FileViewItem>().ToList();
                SelectedItemChanged?.Invoke( sender, selectionAsItems );
            }

            _selectionHandlerRecursionGuard = false;
        }

        #endregion Event handlers
    }

    public abstract class UpdateInfo
    {
        public int Index { get; private set; }

        public UpdateInfo( int index )
        {
            Index = index;
        }

        public abstract FileViewItem CreateViewItem( DepotInfo depot );
    }

    public class UpdateFolderInfo : UpdateInfo
    {
        private readonly string _path;
        private readonly FolderInfo _info;


        public UpdateFolderInfo( int index, string path, FolderInfo info )
            : base( index )
        {
            _path = path;
            _info = info;
        }

        public override FileViewItem CreateViewItem( DepotInfo depot )
        {
            return new FileViewItemDirectory( _path, _info.Name, _info.IsFake );
        }
    }

    public class UpdateFileInfo : UpdateInfo
    {
        private readonly string _path;
        private readonly string _name;
        private readonly FileInfo _info;
        private readonly FileMetaData _p4MetaData;
        private readonly ResourceMetadata _resMetaData;
        private readonly ResourceTypeInfo _typeInfo;

        public UpdateFileInfo( int index, string globalPath, FileInfo info, FileMetaData p4data )
            : base( index )
        {
            string directory = System.IO.Path.GetDirectoryName( globalPath );
            string fileName = System.IO.Path.GetFileName( globalPath );
            string extension = System.IO.Path.GetExtension( globalPath );

            _path = globalPath;
            _name = fileName;
            _p4MetaData = p4data;
            _info = info;
            _resMetaData = ResourceMetadata.GetFor( directory, fileName );
            _typeInfo = ResourceTypeInfo.GetForExtension( extension );
        }

        public override FileViewItem CreateViewItem( DepotInfo depot )
        {
            ImageSource icon = _typeInfo != null ? _typeInfo.Icon : null;

            if ( !string.IsNullOrEmpty( _info.FullThumbnailPath ) && System.IO.File.Exists( _info.FullThumbnailPath ) )
            {
                icon = new BitmapImage( new Uri( _info.FullThumbnailPath ) );
            }

            return new FileViewItemResource( _path, _name, icon, _info.Group, !_info.IsDisabled, depot )
            {
                P4MetaData = _p4MetaData,
                Description = string.IsNullOrEmpty( _info.Description )
                                ? ( _typeInfo != null ? _typeInfo.Name : string.Empty )
                                : _info.Description
            };
        }
    }
}
